using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.DirectoryServices.Protocols;
using InfiniTec.Data;
using InfiniTec.Threading;
using AsyncOperation=InfiniTec.Threading.AsyncOperation;

namespace InfiniTec.DirectoryServices
{
    /// <summary>
    /// This class performs a search on an Active Directory
    /// </summary>
    public sealed class Searcher
    {
        /// <summary>
        /// Returns a constant which defines the default domain as the search root
        /// for a search operation.
        /// </summary>
        public const string DefaultDomain = "_DefaultDomain";

        /// <summary>
        /// Returns a constant which defines the root domain as the search root
        /// for a search operation.
        /// </summary>
        public const string RootDomain = "_RootDomain";

        private readonly AsyncOperationTracker _OperationTracker = new AsyncOperationTracker();
        private readonly List<Item> _Result;
        private readonly List<SortKey> _SortKeys;
        private PropertyDefinitionCollection _PropertiesToLoad;

        /// <summary>
        /// Creates a new instance of this class using the specified settings.
        /// </summary>
        /// <param name="searchRoot">The root of the search operation.</param>
        /// <param name="connection">The <see cref="Connection"/> to use.</param>
        /// <param name="propertiesToLoad">The properties to load. This parameter is optional.</param>
        public Searcher(string searchRoot, Connection connection, params IPropertyDefinition[] propertiesToLoad)
        :
        this(connection, propertiesToLoad)
        {
            SearchRoot = searchRoot;
        }

        /// <summary>
        /// Creates a new instance of this class using the specified settings
        /// </summary>
        /// <param name="connection">The <see cref="Connection"/> to use.</param>
        /// <param name="propertiesToLoad">The properties to load. This parameter is optional.</param>
        public Searcher(Connection connection, params IPropertyDefinition[] propertiesToLoad)
        {
            Connection = connection;
            _Result = new List<Item>();
            _SortKeys = new List<SortKey>();
            Scope = SearchScope.Subtree;
            Initialize(propertiesToLoad);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Searcher"/> class.
        /// </summary>
        /// <param name="connection">The connection to use.</param>
        /// <param name="searchToken">The search token to use.</param>
        public Searcher(Connection connection, SearchToken searchToken) :
        this(connection)
        {
            SearchToken = searchToken;
        }

        /// <summary>
        /// Gets or sets the search token.
        /// </summary>
        /// <value>The search token.</value>
        public SearchToken SearchToken { [DebuggerStepThrough]
        get; [DebuggerStepThrough]
        set; }

        /// <summary>
        /// Gets or sets a value indicating whether the search includes deleted items..
        /// </summary>
        /// <value><see langword="true" /> if deleted items are included in the search; otherwise, <see langword="false" />.</value>
        public bool IncludeDeletedItems { [DebuggerStepThrough]
        get; [DebuggerStepThrough]
        set; }

        /// <summary>
        /// Gets or sets the naming context scope.
        /// </summary>
        /// <value>The naming context scope.</value>
        public NamingContextScope NamingContextScope { [DebuggerStepThrough]
        get; [DebuggerStepThrough]
        set; }

        /// <summary>
        /// Gets the sort keys.
        /// </summary>
        /// <value>The sort keys.</value>
        public IList<SortKey> SortKeys
        {
            [DebuggerStepThrough]
            get { return _SortKeys; }
        }

        /// <summary>
        /// Gets or sets the size of the page.
        /// </summary>
        /// <value>The size of the page.</value>
        public int PageSize { [DebuggerStepThrough]
        get; [DebuggerStepThrough]
        set; }

        /// <summary>
        /// Gets the search result.
        /// </summary>
        /// <value>The search result.</value>
        public IList<Item> SearchResult
        {
            [DebuggerStepThrough]
            get { return _Result; }
        }

        /// <summary>
        /// The distinguished name of the directory entry where the search starts.
        /// </summary>
        public string SearchRoot { [DebuggerStepThrough]
        get; [DebuggerStepThrough]
        set; }

        /// <summary>
        /// Gets or sets the connection to use.
        /// </summary>
        /// <value>The connection to use.</value>
        public Connection Connection { [DebuggerStepThrough]
        get; [DebuggerStepThrough]
        set; }

        /// <summary>
        /// Gets or sets the <see cref="SearchScope"/> of the search operation.
        /// </summary>
        public SearchScope Scope { get; set; }

        /// <summary>
        /// Gets or sets the constraints of the operation.
        /// </summary>
        public string Constraints { get; set; }

        /// <summary>
        /// Provides access to a collection containing the properties to search for.
        /// If this collection is empty, all properties are returned.
        /// </summary>
        public IList<IPropertyDefinition> PropertiesToLoad
        {
            get { return _PropertiesToLoad; }
        }

        /// <summary>
        /// Gets or sets the maximum number of items returned by the search.
        /// </summary>
        public int SizeLimit { get; set; }

        /// <summary>
        /// This event is invoked when the progress of the search operation
        /// changed.
        /// </summary>
        public event ProgressStatusChangedEventHandler FindProgressChanged;

        /// <summary>
        /// This event is invoked when the the search operation has completed.
        /// </summary>
        public event AsyncCompletedEventHandler FindCompleted;

        /// <summary>
        /// Synchronously starts the search operation, retrieving all items.
        /// </summary>
        /// <returns></returns>
        public void FindAll()
        {
            AsyncOperation op = GetFindOperation(SearchMode.FindAll, false);
            op.RunSynchronously();
        }

        /// <summary>
        /// Synchronously starts the search operation, retrieving the next page in the search.
        /// </summary>
        /// <returns></returns>
        public void FindPage()
        {
            AsyncOperation op = GetFindOperation(SearchMode.FindPage, false);
            op.RunSynchronously();
        }

        /// <summary>
        /// Asynchronously starts the search operation.
        /// </summary>
        private void FindAsync(SearchMode mode, bool enablePartialResults)
        {
            AsyncOperation operation;

            operation = GetFindOperation(mode, enablePartialResults);

            operation.Completed += FindCompleted;
            operation.ProgressChanged += FindProgressChanged;

            operation.Run();
        }

        /// <summary>
        /// Returns the search operation used to perform the search based
        /// ojn the specified settings.
        /// </summary>
        /// <returns></returns>
        public AsyncOperation GetFindOperation(SearchMode mode, bool enablePartialResults)
        {
            return new AsyncOperation("Find", SearchOperationInternal(mode, enablePartialResults), _OperationTracker, false, null);
        }

        private IEnumerable<OperationAction> SearchOperationInternal(SearchMode mode, bool enablePartialResults)
        {
            SearchRequest request;
            SearchResponse response;
            List<Item> result;

            do
            {
                if (SearchToken == null && string.IsNullOrEmpty(SearchRoot) && NamingContextScope != NamingContextScope.IncludeSubDomains)
                {
                    throw new ArgumentException("A valid searchroot must be specified when not doing a forest wide search.");
                }

                if (SearchToken != null && !SearchToken.IsValid())
                {
                    throw new InvalidSearchTokenSpecifiedException("The specified search token is not valid");
                }

                if (SearchToken == null) SearchToken = new SearchToken(PageSize);

                if (!SearchToken.IsValid() && SearchRoot == DefaultDomain || SearchRoot == RootDomain)
                {
                    Item root;

                    root = new Item(WellknownDistinguishedNames.RootDse, Connection);

                    if (SearchRoot == DefaultDomain)
                    {
                        root.PropertiesToLoad.Add(WellknownProperties.RootDse.DefaultNamingContext);
                    }
                    else
                    {
                        root.PropertiesToLoad.Add(WellknownProperties.RootDse.RootNamingContext);
                    }

                    AsyncOperation operation = root.GetRefreshOperation();
                    yield return new NestedOperation(operation);

                    if (SearchRoot == DefaultDomain)
                    {
                        SearchRoot = root.Properties.GetProperty(WellknownProperties.RootDse.DefaultNamingContext).Value;
                    }
                    else
                    {
                        SearchRoot = root.Properties.GetProperty(WellknownProperties.RootDse.RootNamingContext).Value;
                    }
                }

                if (!SearchToken.IsValid())
                {
                    request = new SearchRequest(SearchRoot, Constraints, Scope, _PropertiesToLoad.ToStringArray());
                }
                else
                {
                    request = new SearchRequest();
                }

                if (SearchToken.IsValid() || PageSize != 0)
                {
                    var pagingRequest = new PageResultRequestControl(PageSize);
                    if (SearchToken.IsValid())
                    {
                        pagingRequest.Cookie = SearchToken.Token;
                        pagingRequest.PageSize = SearchToken.PageSize;
                    }
                    request.Controls.Add(pagingRequest);
                }

                if (SortKeys.Count > 0) request.Controls.Add(new SortRequestControl(_SortKeys.ToArray()));
                if (NamingContextScope != NamingContextScope.NotSpecified)
                {
                    switch (NamingContextScope)
                    {
                        case NamingContextScope.Domain:
                            request.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope));
                            break;
                        case NamingContextScope.IncludeSubDomains:
                            request.Controls.Add(new SearchOptionsControl(SearchOption.PhantomRoot));
                            break;
                        default:
                            throw new ArgumentException("The specified value of the NamingContextScope is invalid", "NamingContextScope");
                    }
                }
                if (IncludeDeletedItems) request.Controls.Add(new ShowDeletedControl());

                if (SizeLimit > 0) request.SizeLimit = SizeLimit;

                if (enablePartialResults)
                {
                    AsyncOperation<DirectoryResponse> operation =
                    Connection.GetSendRequestOperation(request,
                                                       delegate(object entry)
                                                       {
                                                           SearchResultEntry item;
                                                           item = entry as SearchResultEntry;
                                                           if (item == null) return null;

                                                           return new Item(item.DistinguishedName, item.Attributes, Connection);
                                                       }, null);
                    yield return new NestedOperation(operation);
                    response = (SearchResponse) operation.Result;
                }
                else
                {
                    AsyncOperation<DirectoryResponse> operation = Connection.GetSendRequestOperation(request, null);

                    yield return new NestedOperation(operation);

                    response = (SearchResponse) operation.Result;
                    result = new List<Item>();

                    foreach (SearchResultEntry entry in response.Entries)
                    {
                        result.Add(new Item(entry.DistinguishedName, entry.Attributes, Connection));
                    }

                    _Result.AddRange(result);
                }

                SearchToken.Token = null;
                foreach (DirectoryControl control in response.Controls)
                {
                    PageResultResponseControl prControl;

                    prControl = control as PageResultResponseControl;
                    if (prControl == null) continue;

                    SearchToken.Token = prControl.Cookie;
                }
            } while (mode == SearchMode.FindAll && SearchToken.IsValid());
            if (!SearchToken.IsValid()) SearchToken = null;
        }

        private void Initialize(IEnumerable<IPropertyDefinition> propertiesToLoad)
        {
            _PropertiesToLoad = new PropertyDefinitionCollection(propertiesToLoad);
        }

        /// <summary>
        /// Asynchronously executes the search operation, finding all items.
        /// </summary>
        public void FindAllAsync()
        {
            FindAsync(SearchMode.FindAll, false);
        }

        /// <summary>
        /// Asynchronously executes the search operation, finding the next page of the search.
        /// </summary>
        public void FindPageAsync()
        {
            FindAsync(SearchMode.FindPage, false);
        }
    }
}